/**
 * $RCSfile: JavaMailGateway.java,v $
 * $Revision: 1.28 $
 * $Date: 2001/10/09 21:12:42 $
 *
 * Copyright (C) 1999-2001 CoolServlets Inc. All rights reserved.
 * ===================================================================
 * The Jive Software License (based on Apache Software License, Version 1.1)
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in
 *    the documentation and/or other materials provided with the
 *    distribution.
 *
 * 3. The end-user documentation included with the redistribution,
 *    if any, must include the following acknowledgment:
 *       "This product includes software developed by
 *        Jive Software (http://www.jivesoftware.com)."
 *    Alternately, this acknowledgment may appear in the software itself,
 *    if and wherever such third-party acknowledgments normally appear.
 *
 * 4. The names "Jive" and "CoolServlets" must not be used to
 *    endorse or promote products derived from this software without
 *    prior written permission. For written permission, please
 *    contact webmaster@coolservlets.com.
 *
 * 5. Products derived from this software may not be called "Jive",
 *    nor may "Jive" appear in their name, without prior written
 *    permission of CoolServlets.
 *
 * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED.  IN NO EVENT SHALL COOLSERVLETS INC OR
 * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
 * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
 * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 * ====================================================================
 * This software consists of voluntary contributions made by many
 * individuals on behalf of Jive Software. For more information
 * on Jive Software please visit http://www.jivesoftware.com.
 */

package com.jivesoftware.forum.gateway;

import java.io.*;
import java.util.*;
import java.text.*;
import javax.mail.*;
import javax.mail.internet.*;
import javax.mail.internet.ParseException;
import javax.activation.*;
import com.jivesoftware.forum.*;
import com.jivesoftware.forum.model.ForumNotFoundException;
import com.jivesoftware.forum.model.ResultFilter;
import com.jivesoftware.forum.model.UnauthorizedException;
import com.jivesoftware.util.StringUtils;

/**
 * This class is an abstract JavaMail implementation of the Gateway
 * interface. This class provides all the needed methods to import messages
 * from whatever JavaMail providers are supported.<p>
 *
 * If a provider such as a NNTP provider needs to do something special when
 * connecting or retrieving messages or whatever else all it needs to do is
 * re-implement the method in question in a subclass.<p>
 *
 * The ability to export messages is not handled by this class, if you need to
 * export messages you must extend this class and implement the exportData
 * method or use the {@link com.jivesoftware.forum.util.EmailTask} class
 *
 * @author Bruce Ritchie
 */
public abstract class JavaMailGateway implements Gateway {

    /**
     * protocol to be used by the javamail store mechanism
     */
    protected String protocol = "";

    /**
     * mailbox to be used by the javamail store mechanism
     */
    protected String mailbox = "";

    /**
     * the host that is running the  server daemon
     */
    protected String host = "";

    /**
     * the port that the server daemon is listening to
     */
    protected int port = -1;

    protected String username = null;
    protected String password = null;
    protected boolean deleteEnabled = false;
    protected String defaultFromAddress = null;
    protected boolean emailPrefEnabled = true;
    protected boolean debugEnabled = false;

    // Date parsers that are used when JavaMail fails to parse a date.
    private SimpleDateFormat format1;
    private SimpleDateFormat format2;
    private SimpleDateFormat format3;
    private SimpleDateFormat format4;
    private SimpleDateFormat format5;
    private SimpleDateFormat format6;

    /**
     * used to flag messages in the forum with a message id
     */
    protected String gatewayMessageId = "Message-ID";

    /**
     * used to flag messages in the forum with a parent id
     */
    protected String gatewayParentId = "Parent-ID";

    /**
     * prefix to search for to determine replying messages (lowercase)
     */
    protected String replyPrefix = "re:";

    /**
     * dummy message string for messages autocreated by this gateway
     */
    protected String temporaryParentBody = "";

    /**
     * Used to store the parent message id's indexed by message id
     */
    protected Hashtable parentMessageIDs = new Hashtable();

    /**
     * used to store a messages original date if the date had to be changed
     * such as in the case of a parent with an older child
     */
    public static final String MESSAGE_DATE_HEADER = "Message-Original-Date";

    /**
     * used for storing a hash of a subject as an extended property
     * needed for parent message comparison
     */
    public static final String SUBJECT_EXTENDED_PROPERTY = "Subject-Hash";

    /**
     * used to mark a message as a Jive created message
     */
    public static final String DUMMY_PARENT_HEADER = "Jive-Created-Message";

    protected ForumFactory factory;
    protected long forumID;


    public JavaMailGateway(ForumFactory factory, Forum forum) {
        this.factory = factory;
        this.forumID = forum.getID();
        parentMessageIDs = new Hashtable();
    }

    public synchronized void importData(Date afterDate) throws GatewayException
    {
        // Check to make sure that all required properties are set.
        if (host.equals("") || port == -1 || protocol.equals("") ||
                mailbox.equals(""))
        {
            throw new GatewayException("Required properties are not all set.");
        }

        Store store = null;
        try {
            store = getStore(afterDate);
            List messages = retrieveMessages(store, afterDate);
            resolveParentage(messages);
            correctMessageDates(messages);
            // Sort messages (oldest first by creation date). This is needed so
            // that we don't insert a child before a parent's thread is created,
            // which would be 'A Bad Thing' (tm)
            Collections.sort(messages, new Comparator() {
                public int compare(Object object1, Object object2) {
                    ForumMessage msg1 = (ForumMessage)object1;
                    ForumMessage msg2 = (ForumMessage)object2;
                    return msg1.getCreationDate().compareTo(msg2.getCreationDate());
                }
            });
            importMessages(messages);

            // explicitly nullify to help gc
            messages = null;
        }
        catch (Exception e) {
            throw new GatewayException(e);
        }
        finally {
            try {  store.close();  }
            catch (Exception e) { /* ignore */ }
        }

        // explicitly nullify to help gc
        store = null;
        cleanup();
    }

    public abstract void exportData(ForumMessage message)
        throws GatewayException;

    /**
     * get the JavaMail store
     *
     * @param afterDate The date after which messages will be imported
     * @throws MessagingException if error occurred establishing the connection
     * @return a connected Store object
     */
    protected Store getStore(Date afterDate)
            throws MessagingException
    {
        Session session = Session.getInstance(System.getProperties(), null);
        // Turn on debugging if it's enabled.
        session.setDebug(debugEnabled);
        // Create a Store
        URLName url = new URLName(protocol, host, port, mailbox, username, password);
        Store store = session.getStore(url);
        // Connect the store
        store.connect();

        return store;
    }

    /**
     * Retrieve messages from the JavaMail store
     *
     * @param store      a connected JavaMail store object
     * @param afterDate  the date after which we'll import message
     * @throws MessagingException if a protocol error occurs retrieving a message
     */
    protected List retrieveMessages(Store store, Date afterDate)
            throws MessagingException
    {
        Folder folder = null;
        try {
            // Make sure that can get the default folder.
            if ((folder = store.getFolder(mailbox)) == null) {
                throw new MessagingException("No folder found");
            }

            // Open the folder, get all messages from the server,
            // then weed out the ones we don't want and return the rest.
            if (deleteEnabled) {
                folder.open(Folder.READ_WRITE);
            }
            else {
                folder.open(Folder.READ_ONLY);
            }
            Message [] tempMessages = folder.getMessages();
            List messages = new ArrayList(tempMessages.length);
            // Convert each message to a ForumMessage then add to the list.
            for (int i=0; i < tempMessages.length; i++) {
                // parse the message
                ForumMessage message = parseMessage(tempMessages[i], afterDate);
                if (message != null) {
                    messages.add(message);
                }
                // Some providers require a delete flag to be set
                tempMessages[i].setFlag(Flags.Flag.DELETED, deleteEnabled);
            }
            return messages;
        }
        catch (MessagingException me) {
            throw new MessagingException(me.getMessage());
        }
        finally {
            try { folder.close(deleteEnabled); }
            catch (Exception e) { /* ignore */ }

            // explicitly nullify to help gc
            folder = null;
        }

    }

    /**
     * Resolve parentage
     *
     * We need to go through all the messages and determine
     * parentage from subject if the parent id property isn't already
     * set, which is almost certain in the case of POP3 and sometimes
     * the case with other providers.
     * @param messages a List of ForumMessage objects
     */
    protected void resolveParentage(List messages)
            throws ForumNotFoundException, UnauthorizedException
    {
        Iterator iter               = messages.listIterator();
        Iterator parentIterator     = null;
        ForumMessage message        = null;
        ForumMessage pMessage       = null;
        String messageId            = "";
        String pMessageId           = "";
        String messageSubject       = "";
        String parentSubject        = "";
        int replyIndex              = 0;
        int index                   = 0;
        boolean found               = false;

        while (iter.hasNext()) {
            message = (ForumMessage) iter.next();
            found   = false;

            // has a parent, ignore
            if (message.getProperty(gatewayParentId) != null &&
                    message.getProperty(gatewayParentId).length() > 0) {
                continue;
            }

            messageId      = message.getProperty(gatewayMessageId);
            messageSubject = message.getSubject();
            replyIndex     = messageSubject.toLowerCase().indexOf(replyPrefix);

            // find the subject of the parent that we want to look for
            if (replyIndex > -1) {
                parentSubject = messageSubject.substring(3).trim();
            }
            else {
                parentSubject = messageSubject.trim();
            }

            // check the database for parent message
            // anytime in the last 60 days
            Date min = new Date(message.getCreationDate().getTime() - 60*JiveGlobals.DAY);
            Date max = new Date(message.getCreationDate().getTime() - 1);

            ResultFilter parentCheck = new ResultFilter();
            parentCheck.setModerationRangeMin(Integer.MIN_VALUE+1);
            parentCheck.setCreationDateRangeMin(min);
            parentCheck.setCreationDateRangeMax(max);
            parentCheck.addProperty(SUBJECT_EXTENDED_PROPERTY, StringUtils.hash(parentSubject));

            Forum forum = factory.getForum(forumID);
            if (forum.getMessageCount(parentCheck) > 0) {
                // we found a parent, set the parent messageId
                parentIterator = forum.messages(parentCheck);
                pMessage       = (ForumMessage) parentIterator.next();

                pMessageId = pMessage.getProperty(gatewayMessageId);

                // verify that this isn't the same message being imported yet again
                // if it isn't, update the parent header
                if (!pMessageId.equals(messageId)) {
                    index      = messages.indexOf(message);
                    found      = true;

                    try {
                        message.setProperty(gatewayParentId, pMessageId);
                        messages.set(index, message);
                    }
                    catch (UnauthorizedException ue) { /*ignore */ }
                }
            }

            // if we didn't find in the db, check the messages that
            // we are importing
            if (!found) {
                parentIterator = messages.listIterator();
                while (parentIterator.hasNext() && !found) {

                    pMessage = (ForumMessage) parentIterator.next();

                    // subject fields don't match, next parent
                    if (!pMessage.getSubject().trim().equals(parentSubject)) {
                        continue;
                    }

                    // we found a parent, set the parent messageId
                    pMessageId = pMessage.getProperty(gatewayMessageId);

                    // check to make sure we're not talking about the same
                    // message here
                    if (pMessageId.equals(messageId)) {
                        continue;
                    }

                    // don't allow parent to be younger than child - could
                    // cause circular parentage problems
                    if (pMessage.getCreationDate().compareTo(message.getCreationDate()) >= 0) {
                        continue;
                    }

                    index = messages.indexOf(message);
                    found = true;

                    try {
                        message.setProperty(gatewayParentId, pMessageId);
                        messages.set(index, message);
                    }
                    catch (UnauthorizedException ue) { /*ignore */ }
                }
            }
        }
    }

    /**
     * We need to check dates on all the messages to verify
     * that we don't have a child older than a parent, which Jive was
     * explicitly designed not to handle. Our solution to this problem
     * is to change the parent messages' creation date to 1 second
     * before the child's and store the original date in an extended
     * property.
     *
     * @param messages a List of ForumMessage objects
     */
    protected void correctMessageDates(List messages) {
        Iterator iter               = messages.listIterator();
        Iterator parentIterator     = null;
        ForumMessage message        = null;
        ForumMessage pMessage       = null;
        String messageId            = "";
        String pMessageId           = "";
        boolean parentDateCheckComplete = false;

        // repeatedly go through all the messages checking for
        // parents younger than children.
        while (!parentDateCheckComplete) {
            parentDateCheckComplete = true;

            while (iter.hasNext()) {
                message         = (ForumMessage) iter.next();
                messageId       = message.getProperty(gatewayMessageId);
                pMessageId      = message.getProperty(gatewayParentId);
                parentIterator  = messages.listIterator();

                // no parent, ignore
                if (pMessageId == null || pMessageId.length() < 1) {
                    continue;
                }

                // get the parent message
                while (parentIterator.hasNext()) {
                    pMessage      = (ForumMessage) parentIterator.next();
                    String temp   = pMessage.getProperty(gatewayMessageId);

                    if (!pMessageId.equals(temp)) {
                        pMessage = null;
                    }
                    else {
                        break;
                    }
                }

                // couldn't find the parent
                if (pMessage == null) {
                    continue;
                }

                Date messageDate  = message.getCreationDate();
                Date pMessageDate = pMessage.getCreationDate();
                long time         = pMessage.getCreationDate().getTime();
                String mTime      = String.valueOf(messageDate.getTime());
                int index         = messages.indexOf(message);

                if (pMessageDate.compareTo(messageDate) > 0) {
                    try {
                        // store the original date as a property
                        // set the child's creation date 1 second later
                        // than the parent's
                        message.setProperty(MESSAGE_DATE_HEADER, mTime);
                        message.setCreationDate(new Date(time+1));
                        messages.set(index, message);
                        parentDateCheckComplete = false;
                    }
                    catch (UnauthorizedException ue) { /* ignore */ }
                }
            }
        }
    }

    /**
     * Parse the JavaMail message object and return a ForumMessage object
     *
     * @param message the JavaMail Message object
     * @param afterDate the date we use to check to see if we should import this
     *      message or not
     * @return a ForumMessage object
     */
    protected ForumMessage parseMessage(Message message, Date afterDate) {

        // Create a message (anonymous author)
        ForumMessage forumMessage = factory.createMessage();
        try {
            // too old
            if (getDate(message).compareTo(afterDate) < 0) {
                return null;
            }

            // store all the parent message id's so we can use for threading
            // later
            parentMessageIDs.put(getMessageID(message), getParentMessageID(message));

            // set ForumMessage fields
            forumMessage.setSubject(getSubject(message));
            forumMessage.setProperty(SUBJECT_EXTENDED_PROPERTY, StringUtils.hash(getSubject(message)));
            forumMessage.setCreationDate(getDate(message));
            forumMessage.setModifiedDate(getDate(message));
            forumMessage.setProperty(gatewayMessageId, getMessageID(message));
            forumMessage.setProperty("name", getFromName(message));
            forumMessage.setProperty("email", getFromEmail(message));
            forumMessage.setBody(getBody(message));
            if (!getParentMessageID(message)[0].equals("")) {
                forumMessage.setProperty(gatewayParentId, getParentMessageID(message)[0]);
            }
        }
        catch (MessagingException me) {
            me.printStackTrace();
            // fail this message
            return null;
        }
        catch (NullPointerException npe) {
            npe.printStackTrace();
            // fail this message
            return null;
        }
        catch (IOException ioe) {
            ioe.printStackTrace();
            // fail this message
            return null;
        }
        catch (UnauthorizedException uae) {
            uae.printStackTrace();
            // fail this message
            return null;
        }

        return forumMessage;
    }

    /**
     * Import the List of messages into the forum specified
     *
     * One of the things we need to do in this method is check to see if this
     * message or it's parent already exist (or doesn't exist) in this forum.
     *
     * If a parent doesn't exist, we create a dummy message in its place. If a
     * parent does exist, we attach the message to the parent's thread.
     *
     * If a message already exists in the forum, we first check to see if it's
     * one of the dummy messages we previously created. If so, we modify it.
     * Otherwise, we don't insert the message.
     *
     * If a message doesn't already exist in the forum and it has no parent(s),
     * we create a new thread with this message as the root message
     *
     * @param messages   a list of messages to import into the forum
     */
    protected void importMessages(List messages)
            throws ForumNotFoundException, UnauthorizedException
    {
        Iterator messageIterator = messages.listIterator();
        Forum forum = factory.getForum(forumID);
        ForumMessage message     = null;
        ResultFilter existsCheck = null;
        String messageId         = "";
        String parentId          = "";
        ArrayList insertCache    = new ArrayList();

        // go through each message in the list
        while (messageIterator.hasNext()) {
            message = (ForumMessage) messageIterator.next();

            // failed message - likely couldn't parse a header
            if (message == null) {
                System.err.println("Unabled to import empty message");
                continue;
            }

            try {
                // test to see if this message already exists in this forum
                messageId = message.getProperty(gatewayMessageId);

                // verify that messageId is not null or empty
                // if it is (which it shouldn't be), don't import
                // because you're almost guaranteed to import it
                // more than once
                if (messageId == null || messageId.equals("")) {
                    continue;
                }

                if (!isMessageInDb(forum, gatewayMessageId, messageId)) {

                    // Now, see if the message states that it has a parent
                    parentId = message.getProperty(gatewayParentId);

                    // we have a parent
                    if (parentId != null && !parentId.equals("")) {

                        InsertCacheItem parentItem = null;

                        // first, check the cache
                        if ((parentItem = isMessageinInsertCache(insertCache, parentId)) != null) {

                            // cache insert
                            InsertCacheItem insertCacheItem =
                                    new InsertCacheItem(forum, null, null, message);
                            insertCache.add(insertCacheItem);

                            // update parent so it knows to set the
                            // thread and parent for the child
                            parentItem.addChild(insertCacheItem);
                        }
                        // verify that the parent exists in this forum
                        // yes
                        else if (isMessageInDb(forum, gatewayMessageId, parentId)) {
                            ResultFilter parentCheck = new ResultFilter();
                            parentCheck.setModerationRangeMin(Integer.MIN_VALUE+1);
                            parentCheck.addProperty(gatewayMessageId, parentId);

                            Iterator iter = forum.messages(parentCheck);
                            ForumMessage parent = (ForumMessage) iter.next();
                            ForumThread thread = parent.getForumThread();

                            // cache insert
                            InsertCacheItem insertCacheItem =
                                    new InsertCacheItem(forum, thread, parent, message);
                            insertCache.add(insertCacheItem);
                        }
                        // no
                        else {

                            // create a dummy message as a placeholder with a
                            // creation date 1 second earlier than the child's
                            ForumMessage pMessage = factory.createMessage();
                            long time             = message.getCreationDate().getTime();
                            Date parentDate       = new Date(time - 1);
                            String messageSubject = message.getSubject();
                            String[] parentIDs = (String[]) parentMessageIDs.get(messageId);

                            // find the subject of the parent that we want to create
                            int replyIndex = messageSubject.toLowerCase().indexOf(replyPrefix);

                            if (replyIndex == 0 && messageSubject.length() > 4) {
                                messageSubject = messageSubject.trim().substring(3).trim();
                            }

                            pMessage.setSubject(messageSubject);
                            pMessage.setProperty(SUBJECT_EXTENDED_PROPERTY,
                                                 StringUtils.hash(messageSubject));
                            pMessage.setCreationDate(parentDate);
                            pMessage.setModifiedDate(parentDate);
                            pMessage.setProperty(gatewayMessageId, parentId);
                            pMessage.setProperty(DUMMY_PARENT_HEADER, "true");
                            pMessage.setBody(temporaryParentBody);

                            // determine if this parent is itself a child
                            // yes
                            if (parentIDs.length > 1 &&
                                    isMessageInDb(forum, gatewayMessageId, parentIDs[1])) {
                                ResultFilter parentCheck = new ResultFilter();
                                parentCheck.setModerationRangeMin(Integer.MIN_VALUE+1);
                                parentCheck.addProperty(gatewayMessageId, parentIDs[1]);

                                Iterator pIter = forum.messages(parentCheck);
                                ForumMessage parent = (ForumMessage) pIter.next();
                                ForumThread thread  = parent.getForumThread();

                                // cache insert
                                InsertCacheItem insertCacheItem =
                                        new InsertCacheItem(forum, thread, parent, message);
                                insertCache.add(insertCacheItem);
                            }
                            // no
                            else {
                                // cache insert
                                InsertCacheItem insertCacheItem =
                                        new InsertCacheItem(forum, null, pMessage, message);
                                insertCache.add(insertCacheItem);
                            }
                        }
                    }
                    // we do not have a parent
                    else {
                        // cache insert
                        InsertCacheItem insertCacheItem =
                                new InsertCacheItem(forum, null, null, message);
                        insertCache.add(insertCacheItem);
                    }
                }
                // message does exist, check to make sure it's not a lost parent
                else {
                    existsCheck = new ResultFilter();
                    existsCheck.setModerationRangeMin(Integer.MIN_VALUE+1);
                    existsCheck.addProperty(gatewayMessageId, messageId);
                    existsCheck.addProperty(DUMMY_PARENT_HEADER, "true");

                    // modify existing parent message since we created it
                    if (forum.getMessageCount(existsCheck) > 0) {

                        Iterator iter = forum.messages(existsCheck);
                        ForumMessage msg = (ForumMessage) iter.next();

                        msg.setSubject(message.getSubject());
                        msg.setProperty(SUBJECT_EXTENDED_PROPERTY,
                                        StringUtils.hash(message.getSubject()));
                        msg.setCreationDate(message.getCreationDate());
                        msg.setModifiedDate(message.getModifiedDate());
                        msg.setProperty(gatewayMessageId,
                                message.getProperty(gatewayMessageId));
                        if (message.getProperty(gatewayParentId) != null) {
                            msg.setProperty(gatewayParentId,
                                message.getProperty(gatewayParentId));
                        }
                        msg.deleteProperty(DUMMY_PARENT_HEADER);
                        msg.setProperty("name", message.getProperty("name"));
                        msg.setProperty("email", message.getProperty("email"));
                        msg.setBody(message.getBody());
                    }
                    else { /* do nothing */ }
                }
            }
            catch (UnauthorizedException uae) {
                return;
            }
        }

        // Now insert all the messages
        Iterator cacheIterator = insertCache.iterator();
        InsertCacheItem cacheItem = null;
        while (cacheIterator.hasNext()) {
            cacheItem = (InsertCacheItem) cacheIterator.next();
            cacheItem.insert();
        }

        // explicitly nullify to help gc
        cacheIterator = null;
        insertCache = null;
    }

    protected InsertCacheItem isMessageinInsertCache(List cache, String messageId) {
        Iterator iter = cache.iterator();

        while (iter.hasNext()) {
            InsertCacheItem cacheItem = (InsertCacheItem) iter.next();

            if (cacheItem.containsMessage(messageId)) {
                return cacheItem;
            }
        }
        return null;
    }

    /**
     * utility method to determine if a message exists in the forum already
     * @param key property key to use in the query
     * @param value property value to use in the query
     * @return true if the message was found in the db, false otherwise
     */
    protected boolean isMessageInDb(Forum forum, String key, String value)  {

        if (key == null || key.equals("") || value == null || value.equals("")) {
            return false;
        }

        ResultFilter existsCheck = new ResultFilter();
        existsCheck.setModerationRangeMin(Integer.MIN_VALUE+1);
        existsCheck.addProperty(key, value);

        long messageCount = forum.getMessageCount(existsCheck);

        if (messageCount > 0) {
            return true;
        }
        else {
            return false;
        }
    }

    /**
     * Returns the subject of a JavaMail message.
     *
     * @param message the JavaMail message.
     * @return String the subject of the message.
     * @throws MessagingException if the subject could not be found.
     */
    protected String getSubject(Message message) throws MessagingException {
        String subject = message.getSubject();
        if (subject != null) {
            try {
                return getRFC2047DecodedString(subject);
            }
            catch (Exception e) {
                return subject;
            }
        }
        else {
            String err = "Subject header is unavailable or its value is absent";
            throw new MessagingException(err);
        }
    }

    /**
     * Utility method to decode strings that may have rfc 2047 encoded words
     * in them
     */
    protected String getRFC2047DecodedString(String str) {
        if (str == null) {
            return null;
        }

        if (str.indexOf("=?") != -1 && str.indexOf("?=") != -1) {
            StringBuffer temp = new StringBuffer(str.length());
            int i = 0, first = 0, last = 0;
            String encodedWord = "";

            try {
                while (str.indexOf("=?", i) != -1 && i < str.length()) {
                    first = str.indexOf("=?", i);

                    if (first != i) {
                        // White space between adjacent 'encoded-word's
                        // is not displayed.
                        for (int x = last; x < first; x++) {
                            if (str.charAt(x) != ' ' || i == 0) {
                                temp.append(str.substring(last, first));
                                break;
                            }
                        }
                    }
                    int c = 0;
                    i = first + 2;

                    while (c < 2 && str.indexOf("?", i) != -1) {
                        i = str.indexOf("?", i) + 1;
                        c++;
                    }

                    last = str.indexOf("?=", i);

                    if (last > 0 && last <= str.length()) {
                        last += 2;
                        encodedWord = str.substring(first, last);
                        temp.append(MimeUtility.decodeWord(encodedWord));
                        i = last;
                    }
                    else {
                        // broken encoding
                        throw new ParseException();
                    }
                }

                if (last < str.length()) {
                    temp.append(str.substring(last));
                }

                str = temp.toString();
            }
            catch (ParseException e) {
                System.err.println("pe thrown, encodedWord was " + encodedWord +
                        ", string was " + str);
            }
            catch (UnsupportedEncodingException e) {
                System.err.println("uee thrown, encodedWord was " + encodedWord);
            }
        }

        return str;
    }

    /**
     * Returns the date a message was created or received, or the current date
     * if date parsing fails. POP3 is notorious for improperly formatted date
     * fields and doesn't provide a separate received date field. In many cases
     * JavaMail fails to parse these dates, so we attempt to use our own
     * parsers as backup.
     *
     * @param message a JavaMail message.
     * @return Date the sent (or received) date of the message.
     * @throws MessagingException if an error occurred trying to retrieve the date.
     */
    protected Date getDate(Message message) throws MessagingException {
        Date date = null;

        message = (MimeMessage) message;

        // First, let JavaMail try to parse the date.
        try {
            date = message.getSentDate();
            // If it failed, try the received date
            if (date == null) {
                date = message.getReceivedDate();
            }
        }
        catch (MessagingException me) { /* ignore */ }

        // If JavaMail was able to parse the date, return it.
        if (date != null) {
            return date;
        }
        // Rarely, there will be a date that JavaMail fails to parse. In this
        // case, we'll attempt to parse the date ourselves using some of the
        // more common date formats that JavaMail doesn't recognize.
        else {
            String[] headers = message.getHeader("Date");
            // If no headers exist, there is no chance we can parse ourselves so
            // return the current date.
            if (headers == null || headers[0] == null) {
                return new Date();
            }
            String header = headers[0].trim();
            // Parse date in form: Tues Jan 17 21:10:14 1978
            try {
                if (format1 == null) {
                    format1 = new SimpleDateFormat("EEE MMM dd HH:mm:ss yyyy");
                    format1.setLenient(true);
                }
                return format1.parse(header);
            } catch (java.text.ParseException e1) { }
            // Parse date in form: Wednesday, June 27, 2001
            try {
                if (format2 == null) {
                   format2 = new SimpleDateFormat("EEEE, MMMM dd, yyyy");
                   format2.setLenient(true);
                }
                return format2.parse(header);
            } catch (java.text.ParseException e2) { }
            // Parse date in form: 11/23/97 11:20 PM
            try {
                if (format3 == null) {
                    format3 = new SimpleDateFormat("dd/MM/yy hh:mm a");
                    format3.setLenient(true);
                }
                return format3.parse(header);
            } catch (java.text.ParseException e3) { }
            // Parse date in form: 1999-03-06 17:22:05
            try {
                if (format4 == null) {
                    format4 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
                    format4.setLenient(true);
                }
                return format4.parse(header);
            } catch (java.text.ParseException e4) { }
            // Parse date in form: Fri, 17 May, 1974 09:13:57
            try {
                if (format5 == null) {
                    format5 = new SimpleDateFormat("EEE, dd MMM, yyyy HH:mm:ss");
                    format5.setLenient(true);
                }
                return format5.parse(header);
            } catch (java.text.ParseException e5) { }
            // Parse date in form: November 15, 1978 08:13 PM
            try {
                if (format6 == null) {
                    format6 = new SimpleDateFormat("MMMM dd, yyyy HH:mm a");
                    format6.setLenient(true);
                }
                return format6.parse(header);
            } catch (java.text.ParseException e6) { }

            // All parsing failed so return current date.
            return new Date();
        }
    }

    /**
     * Retrieve the messageID using the JavaMail method
     *
     * @param message    the JavaMail message
     * @return String    the messageID
     * @throws MessagingException if an error occurred trying to retrieve the
     *                            messageid
     */
    protected String getMessageID(Message message) throws MessagingException {

        String messageID = ((MimeMessage) message).getMessageID();

        if (messageID != null && !messageID.equals("")) {
            return messageID;
        }
        else {
            String err = "Message-ID header is unavailable or its value is absent";

            throw new MessagingException(err);
        }
    }

    /**
     * Returns the the parent message id's of a message. Currently, two headers
     * are given for threading purposes, In-Reply-To: and References:
     * In-Reply-To contains the msg id's of all the messages for which
     * this message is a reply (And for which is almost universally only one),
     * References is used to identify a thread of conversation.
     * <p>
     * See RFC 822/2822 for a full explanation. Note that this implementation
     * doesn't handle CFWS, particularly the Comment part. Shoot me.
     * (RFC 2822 Section 3.6.4)
     *
     * @param message the JavaMail message
     * @return String the parent message id (empty if not found)
     * @throws MessagingException if something goes wrong
     */
    protected String[] getParentMessageID(Message message)
            throws MessagingException {

        String headers[] = new String[] {"In-Reply-To", "References"};
        String[] parents = getParentMessageID(message, headers[0]);

        if (parents == null) {
            parents = getParentMessageID(message, headers[1]);
        }

        if (parents == null) {
            return new String[] {""};
        }

        return parents;
    }

    /**
     * Returns the parent ID's of a message as a String [] or null if there are
     * no parents.
     *
     * @param message the JavaMail message.
     * @param header the header to use to lookup the parent message id.
     * @return String the parent message id (null if not found).
     * @throws MessagingException if something goes wrong.
     */
    protected String[] getParentMessageID(Message message, String header)
            throws MessagingException
    {
        String temp [] = message.getHeader(header);
        // The most likely case is that there aren't any parents
        if (temp == null || temp[0] == null || temp[0].equals("")) {
           return null;
        }
        // the last message id is the actual parent to this message, the first
        // is the top parent to this thread
        StringTokenizer st            = new StringTokenizer(temp[0], " ");
        java.util.LinkedList pParents = new java.util.LinkedList();

        // we have more than 1 parent
        // verify format and add to end of List
        if (st.hasMoreTokens()) {
            String t = "";
            while (st.hasMoreTokens()) {
                t = st.nextToken();
                if (t.charAt(0) == '<' && t.charAt(t.length()-1) == '>') {
                    pParents.addFirst(t);
                }
            }
        }
        // single parent, verify format
        else {
            if (temp[0].charAt(0) == '<' &&
                    temp[0].charAt(temp[0].length()-1) == '>') {
                pParents.addFirst(temp[0]);
            }
        }

        if (!pParents.isEmpty()) {
            return (String[]) pParents.toArray(new String[pParents.size()]);
        }
        else {
            return null;
        }
    }

    /**
     * Retrieve the from email name
     * don't let javaMail parse the address since it sometimes
     * won't handle odd addresses like the following one I found
     * "Mike [remove (SPAM_BLOCKER) from my email address to reply]"
     *    <aen(SPAM_BLOCKER)eas@gw(SPAM_BLOCKER)is.com>
     *
     * In most cases we default to returning the whole from string if we can't
     * determine the name because of a poorly formatted header
     *
     * @param message the JavaMail message
     * @return String the from name
     * @throws MessagingException if JavaMail throws an exception
     */
    protected String getFromName(Message message)
            throws MessagingException {

        String[] fromAddresses = message.getHeader("From");
        String fromAddress     = "";
        String fromName        = "";

        if (fromAddresses != null) {
            fromAddress = fromAddresses[0];

            // In format -- First Last <person@place.com>
            if (fromAddress.indexOf("<") != -1) {
                int nameEnd = fromAddress.indexOf("<");
                if (nameEnd > 0) {
                    fromName = fromAddress.substring(0, nameEnd).trim();
                    fromName = StringUtils.replace(fromName, "\"", " ").trim();
                }
                else {
                    fromName = fromAddress;
                }
            }
            // In format -- person@place.com (First Last)
            else if (fromAddress.indexOf("(") != -1) {
                int nameStart   = fromAddress.indexOf("(");
                fromName = fromAddress.substring(nameStart+1, fromAddress.length()-1);

                // broken, try name (
                if (fromName.length() < 1 && nameStart != 0) {
                    fromName = fromAddress.substring(0,nameStart);

                    if (fromName.length() < 1) {
                        fromName = fromAddress;
                    }
                }
            }
            // In format -- person@place.com
            else {
                fromName = fromAddress;
            }

            try {
                return getRFC2047DecodedString(fromName);
            }
            catch (Exception e) {
                return fromName;
            }
        }
        else {
            return "";
        }
    }


    /**
     * Retrieve the from email address
     * don't let javaMail parse the address since it sometimes
     * won't handle odd addresses
     *
     * In most cases we default to returning the whole from string if we can't
     * determine the email address because of a poorly formatted header
     *
     * @param message the JavaMail message
     * @return String the from email address
     * @throws MessagingException if JavaMail throws an exception
     */
    protected String getFromEmail(Message message)
            throws MessagingException {

        String[] fromAddresses = message.getHeader("From");
        int index              = 0;
        String fromAddress     = "";

        if (fromAddresses != null && fromAddresses.length > 0) {
            fromAddress = fromAddresses[0].toString();

            // In format -- First Last <person@place.com>
            // first we check for the existance of quotes, if found
            // we search after them for the string
            if (fromAddress.indexOf("\"") != -1) {
                index = fromAddress.indexOf("\"");
            }

            if (fromAddress.indexOf("<", index) != -1) {
                int nameEnd = fromAddress.indexOf("<");
                int emailEnd = fromAddress.indexOf(">", nameEnd);
                if (emailEnd > 0) {
                    return fromAddress.substring(nameEnd + 1, emailEnd);
                }
                else {
                    return fromAddress;
                }
            }
            // In format -- person@place.com (First Last)
            else if (fromAddress.indexOf("(") != -1) {
                int nameStart = fromAddress.indexOf("(");
                return fromAddress.substring(0, (nameStart != 0) ? nameStart-1 : nameStart).trim();
            }
            // In format -- person@place.com
            else {
                return fromAddress;
            }
        }
        else {
            throw new MessagingException("No From address found!");
        }
    }


    /**
     * Retrieve the textual body of the message.
     *
     * @param message  the JavaMail message
     * @return String  the body of the message
     * @throws MessagingException if JavaMail throws an exception
     * @throws IOException if JavaMail throws an exception handling mime content
     */
    protected String getBody(Message message)
            throws MessagingException, IOException {

        // plain text message
        // search message content for uuencoded attachments, remove them
        // we should handle any octal character instead of just 666 ...
        // however I haven't encountered any other octet yet.
        if (message.isMimeType("text/plain") || message.isMimeType("text")) {
            BufferedReader reader = getTextReader((Part) message);
            String line           = "";
            boolean ignore        = false;
            StringBuffer sb       = new StringBuffer();

            try {
                while ((line = reader.readLine()) != null) {
                    if (line.indexOf("begin 666 ") == 0) {
                        sb.append("[").append(line.substring(9)).append("]\n");
                        ignore = true;
                    }
                    else if (ignore && line.indexOf("end") == 0) {
                        ignore = false;
                    }
                    else if (!ignore) {
                        sb.append(line).append("\n");
                    }
                }
            }
            finally {
                try { reader.close(); }
                catch (Exception e) { /* ignore */ }
            }

            return sb.toString();
        }

        // multipart message
        // save text attachments to the body, other attachments
        // get ignored here
        else if (message.isMimeType("multipart/*")) {
            try {
                return getTextParts((Multipart) message.getContent());
            }
            catch (IllegalArgumentException iae) {
                throw new MessagingException("Character set not recognized", iae);
            }
        }
        // not text, not multipart, likely html
        else {
            return "";
        }
    }

    /**
     * Get a reader object for a Part with the character set properly chosen.
     * Note that the reader object returned should be closed after use.
     *
     * @param part - a javax.mail.part object
     * @return a BufferedReader object
     */
    protected BufferedReader getTextReader(Part part) throws MessagingException {
        try {
            InputStream is = part.getInputStream();
            InputStreamReader reader = null;
            String charset = getTextEncoding(part);

            try {
                reader = new InputStreamReader(is, charset);
            }
            catch (UnsupportedEncodingException ex) {
                reader = null;
            }
            catch (IllegalArgumentException ex) {
                reader = null;
            }

            if (reader == null) {
                charset = MimeUtility.javaCharset("ASCII");
                reader = new InputStreamReader(is, charset);
            }

            BufferedReader bufferedReader = new BufferedReader(reader);
            return bufferedReader;
        }
        catch(IOException ioe) {
            throw new MessagingException(ioe.toString());
        }
    }

    /**
     * Utility method to retrieve the encoding from a part.
     *
     * @param part - a javax.mail.part object
     * @return the encoding, defaulting to ASCII
     */
    protected String getTextEncoding(Part part) {
        String charset = "";

        try {
            // get the character set from the content type
            ContentType xct = new ContentType(part.getContentType().toLowerCase());
            charset = xct.getParameter("charset");

            if (charset != null) {
                charset = MimeUtility.javaCharset(charset);
            }
            else {
                // assume ASCII character encoding
                charset = MimeUtility.javaCharset("ASCII");
            }
        }
        catch (MessagingException e) { /* ignore */ }

        return charset;
    }

    /**
     * Utility method to retrieve text parts of a multipart message
     * and return them
     *
     * @param mp a Multipart object to go through
     * @return String
     * @throws MessagingException if something goes wrong parsing the message
     * @throws IOException if JavaMail throws an exception handling mime content
     */
    private String getTextParts(Multipart mp)
        throws MessagingException, IOException {

        StringBuffer body = new StringBuffer();
        int cnt = mp.getCount();

        for (int j = 0; j < cnt; j++) {
            Part part       = mp.getBodyPart(j);
            String sct      = part.getContentType();
            ContentType ct  = null;

            // no content type?
            if (sct == null) {
                continue;
            }

            ct = new ContentType(sct);

            if (ct.match("text/plain") || ct.match("text")) {
                BufferedReader reader = getTextReader((Part) part);
                String line           = "";

                try {
                    while ((line = reader.readLine()) != null) {
                        body.append(line).append("\n");
                    }
                }
                catch (IOException e) { /* ignore */ }
                finally {
                    try { reader.close(); }
                    catch (Exception e) { /* ignore */ }
                }
            }
            else if (ct.match("multipart/*")) {
                Object o = part.getContent();

                if (o instanceof MimeMultipart) {
                    MimeMultipart mp2 = (MimeMultipart) o;
                    body.append(getTextParts(mp2));
                }
                else {
                    String s = "";
                    if ((s = part.getFileName()) != null) {
                        body.append("[").append(s).append("]");
                    }
                }
            }
        }
        return body.toString();
    }

    protected void cleanup() {
        /* does nothing, subclass may override as necessary */
    }

    /**
     * get the protocol
     *
     * @return the current protocol
     */
    public String getProtocol() {
        return protocol;
    }

    /**
     * set the protocol
     *
     * @param protocol the protocol to use
     */
    public void setProtocol(String protocol) {
        if (protocol != null && !protocol.equals("")) {
            this.protocol = protocol;
        }
    }

    /**
     * get the server port
     *
     * @return the current port setting
     */
    public int getPort() {
        return port;
    }

    /**
     * set the server port
     *
     * @param port the new port setting
     */
    public void setPort(int port) {
        // check for valid port - I believe ports
        // stop at 2^16
        if (port > 1 && port < 65536) {
            this.port = port;
        }
    }

    /**
     * get the server host
     *
     * @return the current host setting (IP or name)
     */
    public String getHost() {
        return host;
    }

    /**
     * set the server host
     *
     * @param host the new host setting
     */
    public void setHost(String host) {
        if (host != null && !host.equals("")) {
            this.host = host;
        }
    }

    /**
     * Returns the username.
     *
     * @return the current username
     */
    public String getUsername() {
        return username;
    }

    /**
     * Sets the username.
     *
     * @param username the username to use
     */
    public void setUsername(String username) {
        this.username = username;
    }

    /**
     * Returns the password.
     *
     * @return the current password
     */
    public String getPassword() {
        return password;
    }

    /**
     * Sets the password.
     *
     * @param password The password to use
     */
    public void setPassword(String password) {
        this.password = password;
    }

    /**
     * Returns the name of the mailbox.
     *
     * @return the current mailbox
     */
    public String getMailbox() {
        return mailbox;
    }

    /**
     * Sets the name of the mailbox.
     *
     * @param mailbox The mailbox to use
     */
    public void setMailbox(String mailbox) {
        if (mailbox != null) {
            this.mailbox = mailbox;
        }
    }

   /**
     * Returns the body that will be used when creating temporary parent
     * messages. It's possible with email accounts and mailing
     * lists to get a response to a message before getting the original message.
     * If this happens and only the child message is available at the time of
     * the gateway import, Jive must still have a way to establish a correct
     * parent/child relationship. Therefore, a temporary fake parent message is
     * created using an extrapolated subject from the child, and a body with the
     * value temporaryParentBody. By default, this value will be
     * the empty String. However, you may wish to create a short message that
     * explains the nature of the fake parent message to the user.
     *
     * On subsequent imports when a real parent message is found, the fake
     * data will be replaced with the correct subject and body.
     *
     * @return the message body that will be used for temporary fake parent
     *      messages.
     */
    public String getTemporaryParentBody() {
        return temporaryParentBody;
    }

    /**
     * Sets the body that will be used when creating temporary parent
     * messages. It's possible with email accounts and mailing
     * lists to get a response to a message before getting the original message.
     * If this happens and only the child message is available at the time of
     * the gateway import, Jive must still have a way to establish a correct
     * parent/child relationship. Therefore, a temporary fake parent message is
     * created using an extrapolated subject from the child, and a body with the
     * value <tt>temporaryParentBody</tt>. By default, this value will be
     * the empty String. However, you may wish to create a short message that
     * explains the nature of the fake parent message to the user.<p>
     *
     * On subsequent imports when a real parent message is found, the fake
     * data will be replaced with the correct subject and body.<p>
     *
     * @param temporaryParentBody the message body that will be used for
     *      temporary fake parent messages.
     */
    public void setTemporaryParentBody(String temporaryParentBody) {
        if (temporaryParentBody != null) {
            this.temporaryParentBody = temporaryParentBody;
        }
    }

    /**
     * Returns true if data will be deleted from the store after being read.
     * When false, no data will be deleted from the store.
     *
     * @return true if data will be deleted from the store after being read.
     */
    public boolean isDeleteEnabled() {
        return deleteEnabled;
    }

    /**
     * Sets whether data will be deleted from the store after being read.
     * When false, no data will be deleted from the store.
     *
     * @param deleteEnabled true if data should be deleted from the store after
     *      being read.
     */
    public void setDeleteEnabled(boolean deleteEnabled) {
        this.deleteEnabled = deleteEnabled;
    }

    /**
     * From email address to send mail from for export
     * Used in the case of anonymous users/users who hide
     * their information
     *
     * @return the current sender
     */
    public String getDefaultFromAddress() {
        return defaultFromAddress;
    }

    /**
     * set the from email address for message export
     *
     * @param address
     */
    public void setDefaultFromAddress(String address) {
        if ("".equals(address)) {
            address = null;
        }
        this.defaultFromAddress = address;
    }

    /**
     * True if a user's privacy setting on their email address should be obeyed
     * when exporting messages.
     *
     * @return true if email privacy settings will be respected.
     */
    public boolean isEmailPrefEnabled() {
        return emailPrefEnabled;
    }

    /**
     * set the whether to honour a user's privacy setting when exporting messages
     *
     * @param address
     */
    public void setEmailPrefEnabled(boolean enabled) {
        this.emailPrefEnabled = enabled;
    }

    /**
     * True if debug is enabled, false otherwise
     */
    public boolean isDebugEnabled() {
        return debugEnabled;
    }

    /**
     * Set whether debug is enabled or not
     *
     * @param boolean true to debug, false otherwise
     */
    public void setDebugEnabled(boolean debugEnabled) {
        this.debugEnabled = debugEnabled;
    }

    /**
     * Small class to cache insert's so that we can do them all at once and
     * not incur penalties for continuously invalidating the message & thread
     * caches because of constant inserts.
     *
     * Allows for the setting of a parent message & thread in children even
     * though the parent may not yet be in the database. The parent will
     * automatically update it's children with the correct thread and parent
     * after inserting itself into the database
     */
    private class InsertCacheItem {

        private ForumThread thread   = null;
        private ForumMessage parent  = null;
        private ForumMessage message = null;
        private Forum forum          = null;
        private ArrayList children   = new ArrayList();

        public InsertCacheItem(Forum forum, ForumThread thread, ForumMessage parent,
                ForumMessage message)
        {
            this.forum   = forum;
            this.thread  = thread;
            this.parent  = parent;
            this.message = message;
        }

        public boolean containsMessage(String messageId) {
            if (message != null && message.getProperty(gatewayMessageId).equals(messageId)) {
                return true;
            }
            else {
                return false;
            }
        }

        public void insert() {
            try {
                if (thread != null && parent != null && message != null) {
                    thread.addMessage(parent, message);
                }
                else if (thread == null && parent != null && message != null) {
                    thread = factory.createThread(parent);
                    forum.addThread(thread);
                    thread.addMessage(parent, message);
                }
                else if (thread == null && parent == null && message != null) {
                    thread = factory.createThread(message);
                    forum.addThread(thread);
                }

                if (children.size() > 0) {
                    Iterator kids = children.iterator();
                    while (kids.hasNext()) {
                        InsertCacheItem child = (InsertCacheItem) kids.next();
                        child.setThread(thread);
                        child.setParent(message);
                    }
                }
            }
            catch (UnauthorizedException e) { /* do nothing */ }
            catch (Exception e) {
                e.printStackTrace();
            }
        }

        public void setThread(ForumThread thread) {
            this.thread = thread;
        }

        public void setParent(ForumMessage parent) {
            this.parent = parent;
        }

        public void addChild(InsertCacheItem child) {
            this.children.add(child);
        }
    }
}